using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Security;
using System.Xml.Linq;
using OrbitOne.Elmah.Data;
using Elmah;
using System.Xml;
using System.Xml.XPath;
using System.IO;
using System.Collections.Specialized;
using ELMAH_Error = OrbitOne.Elmah.Data.ELMAH_Error;
using ErrorFilterRule = OrbitOne.Elmah.Data.ErrorFilterRule;

namespace OrbitOne.Elmah.Logic
{
    /// <summary>
    /// Represents the status of the logged exception
    /// </summary>
    public enum LogStatus
    {
        /// <summary>Exception has been logged correctly.</summary>
        Succeed = 0,
        /// <summary>Exception has already been logged.</summary>
        AlreadyLogged,
        /// <summary>Exception logging failed.</summary>
        Failed
    }

    /// <summary>
    /// Represents an OrbitOneElmahErrorService.
    /// </summary>
    /// <remarks>This inherits from <code>ElmahService:OrbitOneElmahErrorEntity</code></remarks>
    public class OrbitOneElmahErrorService : ElmahService<ELMAH_Error>
    {
        #region Constructor
        /// <summary>
        /// Creates a new OrbitOneElmahErrorService.
        /// </summary>
        public OrbitOneElmahErrorService() { }

        /// <summary>
        /// Creates a new OrbitOneElmahErrorService.
        /// </summary>
        /// <param name="provider">The <code>IElmahDataProvider</code> used by the service.</param>
        public OrbitOneElmahErrorService(ElmahDataProvider provider)
        {
            this.Provider = provider;
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Gets the applications that had errors during the last year
        /// </summary>
        /// <returns></returns>
        public IList<string> GetApplicationsLastYear()
        {
            return
                Provider.Context.ELMAH_Errors.Where(error => error.HappeningTime >= DateTime.Now.AddYears(-1) &&
                                                             error.HappeningTime <= DateTime.Now)
                    .Select(error => error.ApplicationName)
                    .Distinct()
                    .ToList();
        }

        /// <summary>
        /// Get all the applications with there error count for authenticated or not authenticated users after a specified date.
        /// </summary>
        /// <param name="dateTime">The dateTime that errors should fall after.</param>
        /// <param name="authenticated">Indication wether the errors should be for authenticated users or anonymous users.</param>
        /// <returns>An <code>IDictionary:string,int</code> 
        /// where the key is the application name and the value is the error count for that application.</returns>
        /// <remarks>Only on the errors where the ErrorType is 'Code' and the ErrorLevel is greater then 0</remarks>
        public IDictionary<string, int> GetApplicationErrorCount(DateTime dateTime, bool authenticated)
        {
            var query = GetAllCodeErrorsHigherThenLevel0After(dateTime);

            query = authenticated
                        ? query.Where(error => error.UserName != "")
                        : query.Where(error => error.UserName == "");

            return query.ToList().GroupBy(error => error.ApplicationName)
                .OrderByDescending(pair => pair.Count())
                .ToDictionary(pair => pair.Key, pair => pair.Count());
        }

        /// <summary>
        /// Get the message error counts after a specified date.
        /// </summary>
        /// <param name="dateTime">The dateTime that errors should fall after.</param>
        /// <returns>An <code>IDictionary:OrbitOneElmahErrorEntity,int</code>
        /// where the key is the first OrbitOneElmahErrorEntity and the value 
        /// is the count of the nr of occurrences of the keys Error message.</returns>
        /// <remarks>Only on the errors where the ErrorType is 'Code' and the ErrorLevel is greater then 0</remarks>
        public IDictionary<ELMAH_Error, int> GetMessageErrorCounts(DateTime dateTime)
        {
            return GetApplicationMessageErrorCount(dateTime, null);
        }

        /// <summary>
        /// Get the message error counts after a specified date, and for a specified applcition.
        /// </summary>
        /// <param name="dateTime">The dateTime that errors should fall after.</param>
        /// <param name="application">The application for which you want to find the errors.</param>
        /// <returns>An <code>IDictionary:OrbitOneElmahErrorEntity,int</code>
        /// where the key is the first OrbitOneElmahErrorEntity and the value 
        /// is the count of the nr of occurrences of the keys Error message.</returns>
        /// <remarks>Only on the errors where the ErrorType is 'Code' and the ErrorLevel is greater then 0</remarks>
        public IDictionary<ELMAH_Error, int> GetApplicationMessageErrorCount(DateTime dateTime, string application)
        {
            var q = GetAllCodeErrorsHigherThenLevel0After(dateTime)
                .Where(error => string.IsNullOrEmpty(application) || error.ApplicationName == application);

            var kv = q.GroupBy(error => error.ErrorMessage)
                .Select(errorGroup =>
                        new KeyValuePair<string, int>(
                            errorGroup.Key,
                            errorGroup.Count()));

            IDictionary<ELMAH_Error, int> retval = new Dictionary<ELMAH_Error, int>();

            foreach (var pair in kv)
            {
                var key = pair.Key;
                retval.Add(q.Where(error => error.ErrorMessage == key)
                               .OrderByDescending(error => error.HappeningTime)
                               .First()
                           , pair.Value);
            }

            return retval.OrderByDescending(pair => pair.Value).ToDictionary(pair => pair.Key, pair => pair.Value);
        }

        /// <summary>
        /// Get the Error count grouped by application after a specified date.
        /// </summary>
        /// <param name="dateTime">The dateTime that errors should fall after.</param>
        /// <returns>An <code>IDictionary:string,int</code>
        /// where the key is the application name and the value is the error count for that application.</returns>
        /// <remarks>Only on the errors where the ErrorType is 'Code' and the ErrorLevel is greater then 0</remarks>
        public IDictionary<string, int> GetErrorCountGroupedByApplication(DateTime dateTime)
        {
            var retval = GetAllCodeErrorsHigherThenLevel0After(dateTime)
                .GroupBy(error => error.ApplicationName)
                .Select(errorGroup => new KeyValuePair<string, int>(errorGroup.Key, errorGroup.Count()))
                .ToList();

            return retval.OrderByDescending(pair => pair.Value).ToDictionary(pair => pair.Key, pair => pair.Value);
        }

        /// <summary>
        /// Get the error counts grouped by application after a specified date.
        /// </summary>
        /// <param name="dateTime">The dateTime that errors should fall after.</param>
        /// <returns>An <code>IList:UrlErrorCount</code></returns>
        /// <remarks>Only on the errors where the ErrorType is 'Code' and the ErrorLevel is greater then 0</remarks>
        public IList<UrlErrorCount> GetUrlErrorCountsForMessagesGroupedByUrl(DateTime dateTime)
        {
            return GetAllCodeErrorsHigherThenLevel0After(dateTime).ToList()
                .GroupBy(error => new
                                      {
                                          error.ApplicationName,
                                          Url = error.AllXml
                                      .DescendantsAndSelf("error")
                                      .Descendants("serverVariables")
                                      .Descendants("item")
                                      .Where(el => el.Attribute("name") != null && el.Attribute("name").Value == "URL")
                                      .Descendants("value")
                                      .Attributes("string").FirstOrDefault().Value
                                      })
                .Select(errorGroup => new UrlErrorCount(errorGroup.OrderByDescending(error => error.HappeningTime).FirstOrDefault(), errorGroup.Key.Url, errorGroup.Count()))
                .OrderByDescending(urlErrorCount => urlErrorCount.ErrorCount)
                .ToList();
        }


        /// <summary>
        /// Get the applications with their health states after a specified of the 24 hours before the today.
        /// </summary>
        /// <returns>An <code>IDictionary:string,double</code>
        /// where the key is the application name and the value is the applications health value</returns>
        /// <remarks>The applications health state is calculated as following:
        /// (application error count  / nr of total hours) * 
        /// (1 + applications distinct(ErrorMessage) error count / 10) * 
        /// (application's average error level) </remarks>
        public IDictionary<string, double> GetApplicationsHealth()
        {
            DateTime end = DateTime.Now;
            return GetApplicationsHealth(end.AddDays(-1), end);
        }
        /// <summary>
        /// Get the applications with their health states after a specified of the 24 hours before the today.
        /// </summary>
        /// <returns>An <code>IDictionary:string,double</code>
        /// where the key is the application name and the value is the applications health value</returns>
        /// <remarks>The applications health state is calculated as following:
        /// (application error count  / nr of total hours) * 
        /// (1 + applications distinct(ErrorMessage) error count / 10) * 
        /// (application's average error level) </remarks>
        public IDictionary<string, double> GetApplicationsHealth(TimeSpan duration)
        {
            return GetApplicationsHealth(DateTime.Now.Subtract(duration), DateTime.Now);
        }
        public IDictionary<string, double> GetApplicationsHealth(TimeSpan duration, DateTime end)
        {
            return GetApplicationsHealth(DateTime.Now.Subtract(duration), end);
        }

        /// <summary>
        /// Get the applications with their health states after a specified of the 24 hours before the specified date.
        /// </summary>
        /// <param name="end">The health states of the applications that fall between the specified date and 1 day before that date.</param>
        /// <returns>An <code>IDictionary:string,double</code>
        /// where the key is the application name and the value is the applications health value</returns>
        /// <remarks>The applications health state is calculated as following:
        /// (application error count  / nr of total hours) * 
        /// (1 + applications distinct(ErrorMessage) error count / 10) * 
        /// (application's average error level) </remarks>
        public IDictionary<string, double> GetApplicationsHealth(DateTime start, DateTime end)
        {
            var oneYear = Provider.Context.ELMAH_Errors.Where(error => error.HappeningTime > start && error.HappeningTime <= end)
                .Where(error => error.ErrorLevel > 0)
                .Select(error => new
                                     {
                                         error.HappeningTime,
                                         error.ApplicationName,
                                         error.ErrorMessage,
                                         error.ErrorLevel
                                     }
                )
                .ToList();

            var applications = from error in oneYear
                               group error by error.ApplicationName
                                   into errorGroup
                                   select new
                                              {
                                                  ApplicationName = errorGroup.Key,
                                                  ErrorCount = errorGroup.Count(),
                                                  AverageErrorLevel = errorGroup.Select(e => e.ErrorLevel)
                                   .Average(),
                                                  DistinctApplicationErrorCount = errorGroup.Select(error => error.ErrorMessage)
                                   .Distinct()
                                   .Count()
                                              };

            var result = applications.ToDictionary(application => application.ApplicationName,
                                                   application => application.ErrorCount / (end - start).TotalHours *
                                                                  (1 + application.DistinctApplicationErrorCount / 10) *
                                                                  application.AverageErrorLevel);
            return result;
        }

        /// <summary>
        /// Gets the application and there errors for the last day (before today)
        /// </summary>
        /// <returns>An <code>IDictionary:string,int</code>
        /// where the key is the application name and the value is the nr of errors for that application during the last day.</returns>
        /// <remarks>On errors where the errorLevel is greater then 0</remarks>
        public IDictionary<string, int> GetTopApplications()
        {
            return GetTopApplications(DateTime.Now);
        }

        /// <summary>
        /// Gets the application and there errors for the last day (before the specified date)
        /// </summary>
        /// <param name="end">The applications that fall between the specified date and 1 day before that date.</param>
        /// <returns>An <code>IDictionary:string,int</code>
        /// where the key is the application name and the value is the nr of errors for that application during the last day.</returns>
        /// <remarks>On errors where the errorLevel is greater then 0</remarks>
        public IDictionary<string, int> GetTopApplications(DateTime end)
        {
            var start = end.AddDays(-1);
            var apps = Provider.Context.ELMAH_Errors.Where(error => error.HappeningTime > start && error.HappeningTime <= end)
                .Where(error => error.ErrorLevel > 0)
                .GroupBy(error => error.ApplicationName)
                .Select(group => new
                                     {
                                         Application = group.Key,
                                         Errors = group.Count()
                                     })
                .ToList();
            return apps.FirstOrDefault() != null
                       ? apps.OrderByDescending(group => group.Errors).ToDictionary(group => group.Application, pair => pair.Errors)
                       : null;
        }

        /// <summary>
        /// Get the last 100 errors
        /// </summary>
        /// <returns>An <code>IList:OrbitOneElmahErrorFacade</code></returns>
        public IList<OrbitOneElmahErrorFacade> GetLast100Errors()
        {
            return Provider.Context.ELMAH_Errors
                .OrderByDescending(error => error.HappeningTime)
                .Select(error => new OrbitOneElmahErrorFacade(error.ErrorId,
                                                              error.HappeningTime,
                                                              error.HostName,
                                                              error.ApplicationName,
                                                              error.ErrorMessage,
                                                              error.ErrorType,
                                                              error.ErrorLevel,
                                                              error.UserName,
                                                              error.AppliedRules))
                .Take(100)
                .ToList();
        }

        /// <summary>
        /// Get the errors on which the search filter's parameters are applicable.
        /// </summary>
        /// <param name="searchFilter"></param>
        /// <returns>An <code>IEnumerable:SearchRecord</code></returns>
        public SearchRecordCollection SearchErrors(ErrorSearchFilter searchFilter, int startRowIndex, int maximumRows)
        {
            return SearchErrors(searchFilter, null, startRowIndex, maximumRows);
        }

        /// <summary>
        /// Get the errors on which the search filter's parameters are applicable before a specified date.
        /// </summary>
        /// <param name="searchFilter"></param>
        /// <param name="end">The errors should occurr before the date</param>
        /// <returns>An <code>IEnumerable:SearchRecord</code></returns>
        public SearchRecordCollection SearchErrors(ErrorSearchFilter searchFilter, DateTime? end, int startRowIndex, int maximumRows)
        {
            return searchFilter.GetSearchView(Provider.Context.ELMAH_Errors, end, startRowIndex, maximumRows);
        }

        /// <summary>
        /// Saves the error (base on the errorXml) to the database with id (errorId)
        /// </summary>
        /// <param name="errorXml"></param>
        /// <param name="errorId"></param>
        /// <returns></returns>
        public LogStatus SaveError(string errorXml, Guid errorId)
        {
            if (errorXml == null) throw new ArgumentNullException("errorXml");
            var sett = new XmlReaderSettings { CheckCharacters = false };
            var reader = XmlReader.Create(new StringReader(errorXml), sett);
            var error = new Error();
            error.FromXml(reader);

            var orbitOneElmahError = new ELMAH_Error
                                         {
                                             ErrorId = errorId,
                                             AllXml = XElement.Parse(errorXml),
                                             ApplicationName = error.ApplicationName,
                                             ErrorMessage = error.Message,
                                             HostName = error.HostName,
                                             ExceptionType = error.Type,
                                             Source = error.Source,
                                             UserName = error.User,
                                             StatusCode = error.StatusCode,
                                             HappeningTime = error.Time
                                         };

            var rules = GetAppliedRules(orbitOneElmahError);

            if (rules.Count() == 0)
            {
                orbitOneElmahError.ErrorLevel = 5;
                orbitOneElmahError.ErrorType = ErrorSearchFilter.ErrorTypeEnum.Code.ToString();
            }
            else
            {
                orbitOneElmahError.ErrorLevel = rules.Min(rule => rule.Level);
                orbitOneElmahError.ErrorType = rules.Any(rule => rule.Type == ErrorSearchFilter.ErrorTypeEnum.Infrastructure.ToString())
                                                   ? ErrorSearchFilter.ErrorTypeEnum.Infrastructure.ToString()
                                                   : ErrorSearchFilter.ErrorTypeEnum.Code.ToString();
                orbitOneElmahError.AppliedRules = string.Join(";",
                                                              rules.Select(rule => string.Format("{0}-{1}",
                                                                                                 rule.Name,
                                                                                                 rule.Level))
                                                                  .ToArray());
            }
            LogStatus result;
            try
            {
                result = Save(orbitOneElmahError);
            }
            catch (Exception e)
            {
                result = FailedSaveError(e);
            }

            return result;
        }
        #endregion

        #region Private Methods
        private LogStatus FailedSaveError(Exception e)
        {
            Log.Error("Failed on exception save: " + e);
            return LogStatus.Failed;
        }

        private IEnumerable<ErrorFilterRule> GetAppliedRules(ELMAH_Error error)
        {
            _logger.Debug("Getting Applied rules");

            XDocument doc = XDocument.Parse(error.AllXml.ToString());

            var xpaths =
                Provider.Context.XPaths.ToList().Where(x => doc.XPathSelectElements(x.XPathString).Count() != 0).
                    ToList();
            var result = xpaths.Join(Provider.Context.ErrorFilterRuleXPathMaps, x => x.Id, e => e.XPathId, (x, e) => e)
                .Join(Provider.Context.ErrorFilterRules, m => m.ErrorFilterRuleId, e => e.Id, (m, e) => e);
            return result;
        }

        /// <summary>
        /// Get all the errors where the ErrorType is 'Code' and the ErrorLevel is greater then 0 that fall after the given date
        /// </summary>
        /// <param name="dateTime">The dateTime that errors should fall after.</param>
        /// <returns>an <code>IQueryable:OrbitOneElmahErrorEntity</code></returns>
        private IQueryable<ELMAH_Error> GetAllCodeErrorsHigherThenLevel0After(DateTime dateTime)
        {
            return Provider.Context.ELMAH_Errors
                .Where(error => error.HappeningTime >= dateTime &&
                                error.ErrorLevel > 0 &&
                                (error.AppliedRules == null ||
                                 (error.AppliedRules != null &&
                                  !error.AppliedRules.Contains(Constants.RuleNameBots))) &&
                                error.ErrorType == ErrorSearchFilter.ErrorTypeEnum.Code.ToString());
        }
        #endregion

        public LogStatus Save(ELMAH_Error error)
        {
            LogStatus result;
            if (!Provider.Context.ELMAH_Errors.Contains(error))
            {
                Provider.Context.ELMAH_Errors.InsertOnSubmit(error);
                result = LogStatus.Succeed;
            }
            else
            {
                result = LogStatus.AlreadyLogged;
            }
            Provider.Context.SubmitChanges();
            return result;
        }
    }
}